﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace EpiMapit
{
	public class CaseSet
	{
		private Dictionary<string, int> levels;
        private int numberOfJoins;

		public CaseSet()
		{
			levels = new Dictionary<string, int>();
            HasDate = true;
            numberOfJoins = 1;
		}

        public int NumberOfJoins
        {
            get { return numberOfJoins; }
        }

        public float Lat
		{
			get;
			set;
		}

        public float Lon
		{
			get;
			set;
		}

        public string RegionCode
        {
            set
            {
                RegionCodes = new HashSet<string>();
                RegionCodes.Add(value);
            }
            get
            {
                if (RegionCodes.Count == 1)
                    return RegionCodes.First();
                else
                    throw new Exception("Wrong use of RegionCode: must only be used if RegionCodes.Count == 1");
            }
        }

        public HashSet<string> RegionCodes
        {
            get;
            set;
        }

        public DateTime Date
        {
            get;
            set;
        }

        public bool HasDate
        {
            get;
            set;
        }


        /// <summary>
		/// Dictionary with type of case (confirmed, expected, ...) and cases (10, 23, ...)
		/// </summary>
		public Dictionary<string, int> Levels
		{
			get { return levels; } set { this.levels = value; }
		}

        public float DistanceFrom(CaseSet set)
		{
			return Distance(Lat, Lon, set.Lat, set.Lon, 'K');
		}

		/// <summary>
		/// Sums the cases and point the lat long in the middle
		/// </summary>
		/// <param name="set"></param>
		/// <returns></returns>
		public void JoinWith(CaseSet other)
		{
            var totalNumberOfJoins = NumberOfJoins + other.NumberOfJoins;
            
            this.Lat = Lat * NumberOfJoins / totalNumberOfJoins + other.Lat * other.NumberOfJoins / totalNumberOfJoins;
            this.Lon = Lon * NumberOfJoins / totalNumberOfJoins + other.Lon * other.NumberOfJoins / totalNumberOfJoins;
            
            foreach (var key in other.Levels.Keys)
			{
				if (levels.ContainsKey(key))
				{
					levels[key] += other.Levels[key];
				}
				else
				{
					levels.Add(key, other.Levels[key]);
				}
			}
            this.RegionCodes.AddRange(other.RegionCodes);

            numberOfJoins = totalNumberOfJoins;
		}

		/// <summary>
		/// Distance between two points. 
		/// </summary>
		/// <param name="lat1"></param>
		/// <param name="lon1"></param>
		/// <param name="lat2"></param>
		/// <param name="lon2"></param>
		/// <param name="unit">K for Kilometers, N for nautic miles, otherwise miles</param>
		/// <returns></returns>
		private float Distance(float lat1, float lon1, float lat2, float lon2, char unit)
		{
			float theta = lon1 - lon2;
			float dist = (float) (Math.Sin(Deg2Rad(lat1)) * Math.Sin(Deg2Rad(lat2)) + Math.Cos(Deg2Rad(lat1)) * Math.Cos(Deg2Rad(lat2)) * Math.Cos(Deg2Rad(theta)));
			dist = (float)Math.Acos(dist);
			dist = Rad2Deg(dist);
			dist = dist * 60.0f * 1.1515f;
			if (unit == 'K')
			{
				dist = dist * 1.609344f;
			}
			else if (unit == 'N')
			{
				dist = dist * 0.8684f;
			}
			return dist;
		}

		/// <summary>
		/// Converts decimal degrees to radians
		/// </summary>
		/// <param name="deg"></param>
		/// <returns></returns>
		private float Deg2Rad(float deg)
		{
			return (deg * (float)Math.PI / 180.0f);
		}

		/// <summary>
		/// Converts radians to decimal degrees
		/// </summary>
		/// <param name="rad"></param>
		/// <returns></returns>
		private float Rad2Deg(float rad)
		{
			return (rad / (float)Math.PI * 180.0f);
		}
		
		public float TotalSize()
		{
			return levels.Sum(x => x.Value);
		}

        public override string ToString()
        {
            return string.Join(" - ", RegionCodes.ToArray()) + " on " + Date + ": " + LevelsToString();
        }

        private string LevelsToString()
        {
            StringBuilder sb = new StringBuilder();

            bool first = true;
            foreach (var entry in Levels)
            {
                if (entry.Value == 0)
                    continue;

                if (!first)
                    sb.Append(", ");
                first = false;

                sb.Append(entry.Key);
                sb.Append("=");
                sb.Append(entry.Value);
            }

            return sb.ToString();
        }

        public CaseSet Clone()
        {
            return new CaseSet()
            {
                Date = this.Date,
                HasDate = this.HasDate,
                Lat = this.Lat,
                Levels = this.Levels.Clone(),
                Lon = this.Lon,
                RegionCodes = this.RegionCodes.Clone(),
            };
        }
    }
}
